// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util.io

import com.intellij.openapi.util.io.IoTestUtil.assumeSymLinkCreationIsSupported
import com.intellij.openapi.util.io.NioFiles
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.ByteArrayInputStream
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermissions
import java.util.jar.Attributes
import java.util.jar.JarFile
import java.util.jar.Manifest
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import kotlin.io.path.createDirectories
import kotlin.io.path.createFile
import kotlin.io.path.createParentDirectories
import kotlin.io.path.createSymbolicLinkPointingTo
import kotlin.io.path.getPosixFilePermissions
import kotlin.io.path.inputStream
import kotlin.io.path.name
import kotlin.io.path.writeText

class CompressorTest {
  @Test fun simpleZip(@TempDir tempDir: Path) {
    val zip = tempDir.resolve("test.zip")
    val data = tempDir.resolve("file.txt").apply { writeText("789") }
    Compressor.Zip(zip).use {
      it.addFile("empty.txt", byteArrayOf())
      it.addFile("file1.txt", "123".toByteArray())
      it.addFile("file2.txt", ByteArrayInputStream("456".toByteArray()))
      it.addFile("file3.txt", data)
    }
    assertZip(zip, "empty.txt" to "", "file1.txt" to "123", "file2.txt" to "456", "file3.txt" to "789")
  }

  @Test fun simpleTar(@TempDir tempDir: Path) {
    val tar = tempDir.resolve("test.tar")
    val data = tempDir.resolve("file.txt").apply { writeText("789") }
    Compressor.Tar(tar, Compressor.Tar.Compression.GZIP).use {
      it.addFile("empty.txt", byteArrayOf())
      it.addFile("file1.txt", "123".toByteArray())
      it.addFile("file2.txt", ByteArrayInputStream("456".toByteArray()))
      it.addFile("file3.txt", data)
    }
    assertTar(tar, "empty.txt" to "", "file1.txt" to "123", "file2.txt" to "456", "file3.txt" to "789")
  }

  @Test fun simpleZipWithFilters(@TempDir tempDir: Path) {
    val zip = tempDir.resolve("test.zip")
    val set = mutableSetOf<String>()
    Compressor.Zip(zip).filter { entryName, _ -> set.add(entryName) && !entryName.startsWith("d1/") }.use {
      it.addFile("file1.txt", "123".toByteArray())
      it.addFile("file2.txt", "456".toByteArray())
      it.addFile("file1.txt", "789".toByteArray())
      it.addFile("d1/d11/f.txt", "-".toByteArray())
      it.addDirectory("d1/d12")
    }
    assertZip(zip, "file1.txt" to "123", "file2.txt" to "456")
  }

  @Test fun streamZip(@TempDir tempDir: Path) {
    val zip = tempDir.resolve("test.zip")
    zip.outputStream().use { os ->
      Compressor.Zip(os).withLevel(ZipEntry.STORED).use {
        it.addFile("file.txt", "123".toByteArray())
      }
    }
    assertZip(zip, "file.txt" to "123")
  }

  @Test fun recursiveZip(@TempDir tempDir: Path) {
    val dir = tempDir.resolve("dir")
    tempDir.resolve("dir/f1").createParentDirectories().writeText("1")
    tempDir.resolve("dir/f2").createParentDirectories().writeText("2")
    tempDir.resolve("dir/d1/f11").createParentDirectories().writeText("11")
    tempDir.resolve("dir/d1/f12").createParentDirectories().writeText("12")
    tempDir.resolve("dir/d1/d11/f111").createParentDirectories().writeText("111")
    tempDir.resolve("dir/d1/d11/f112").createParentDirectories().writeText("112")
    tempDir.resolve("dir/d2/f21").createParentDirectories().writeText("21")
    tempDir.resolve("dir/d2/f22").createParentDirectories().writeText("22")

    val zip = tempDir.resolve("test.zip")
    Compressor.Zip(zip).filter { entryName, _ -> entryName != "d1/d11" }.use { it.addDirectory(dir) }
    assertZip(
      zip,
      "d1/" to "", "d2/" to "",
      "f1" to "1", "f2" to "2",
      "d1/f11" to "11", "d1/f12" to "12",
      "d2/f21" to "21", "d2/f22" to "22")
  }

  @Test fun recursiveTarWithPrefix(@TempDir tempDir: Path) {
    val dir = tempDir.resolve("dir")
    tempDir.resolve("dir/f1").createParentDirectories().writeText("1")
    tempDir.resolve("dir/f2").createParentDirectories().writeText("2")
    tempDir.resolve("dir/d1/f11").createParentDirectories().writeText("11")
    tempDir.resolve("dir/d1/f12").createParentDirectories().writeText("12")
    tempDir.resolve("dir/d1/d11/f111").createParentDirectories().writeText("111")
    tempDir.resolve("dir/d1/d11/f112").createParentDirectories().writeText("112")
    tempDir.resolve("dir/d2/f21").createParentDirectories().writeText("21")
    tempDir.resolve("dir/d2/f22").createParentDirectories().writeText("22")

    val tar = tempDir.resolve("test.tgz")
    Compressor.Tar(tar, Compressor.Tar.Compression.GZIP).use { it.addDirectory("tar/", dir) }
    assertTar(
      tar,
      "tar/" to "", "tar/d1/" to "", "tar/d1/d11/" to "", "tar/d2/" to "",
      "tar/f1" to "1", "tar/f2" to "2",
      "tar/d1/f11" to "11", "tar/d1/f12" to "12",
      "tar/d1/d11/f111" to "111", "tar/d1/d11/f112" to "112",
      "tar/d2/f21" to "21", "tar/d2/f22" to "22")
  }

  @Test fun tarWithEmptyPrefix(@TempDir tempDir: Path) {
    val file = tempDir.resolve("dir/file").createParentDirectories().createFile()
    val tar = tempDir.resolve("test.tgz")
    Compressor.Tar(tar, Compressor.Tar.Compression.GZIP).use { it.addDirectory("", file.parent) }
    assertTar(tar, file.name to "")
  }

  @Test fun tarWithExecutableFiles(@TempDir tempDir: Path) {
    assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"))

    val dir = tempDir.resolve("dir").createDirectories()
    val regular = dir.resolve("regular").createFile()
    val executable = dir.resolve("executable").createFile(PosixFilePermissions.asFileAttribute(PosixFilePermission.entries.toSet()))

    val tar = tempDir.resolve("test.tgz")
    Compressor.Tar(tar, Compressor.Tar.Compression.GZIP).use { it.addDirectory(dir) }
    val out = tempDir.resolve("out")
    Decompressor.Tar(tar).extract(out)
    assertThat(out.resolve(regular.name).getPosixFilePermissions()).doesNotContain(PosixFilePermission.OWNER_EXECUTE)
    assertThat(out.resolve(executable.name).getPosixFilePermissions()).contains(PosixFilePermission.OWNER_EXECUTE)
  }

  @Test fun tarWithSymbolicLinks(@TempDir tempDir: Path) {
    assumeSymLinkCreationIsSupported()

    val dir = tempDir.resolve("dir").createDirectories()
    val origin = dir.resolve("origin").createFile()
    val link = dir.resolve("link").createSymbolicLinkPointingTo(origin.fileName)

    val tar = tempDir.resolve("test.tgz")
    Compressor.Tar(tar, Compressor.Tar.Compression.GZIP).use { it.addDirectory(dir) }
    NioFiles.deleteRecursively(dir)

    val out = tempDir.resolve("out")
    Decompressor.Tar(tar).extract(out)
    assertThat(out.resolve(link.name)).isSymbolicLink.hasSameBinaryContentAs(out.resolve(origin.name))
  }

  @Test fun entryNameTrimming(@TempDir tempDir: Path) {
    val zip = tempDir.resolve("test.zip")
    Compressor.Zip(zip).use { it.addFile("//file.txt//", "123".toByteArray()) }
    assertZip(zip, "file.txt" to "123")
  }

  @Test fun jarWithManifest(@TempDir tempDir: Path) {
    val jar = tempDir.resolve("test.jar")
    val mf = Manifest()
    mf.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "9.75"
    Compressor.Jar(jar).use { it.addManifest(mf) }
    assertZip(jar, JarFile.MANIFEST_NAME to "Manifest-Version: 9.75")
  }

  //<editor-fold desc="Helpers.">
  private fun assertZip(zip: Path, vararg expected: Pair<String, String>) {
    val actual = ZipInputStream(zip.inputStream()).use {
      generateSequence(it::getNextEntry).map { entry -> entry.name to it.readBytes().toString(StandardCharsets.UTF_8).trim() }.toList()
    }
    assertThat(actual).containsExactlyInAnyOrder(*expected)
  }

  private fun assertTar(tar: Path, vararg expected: Pair<String, String>) {
    val actual = TarArchiveInputStream(GzipCompressorInputStream(tar.inputStream())).use {
      generateSequence(it::getNextEntry).map { entry -> entry.name to it.readBytes().toString(StandardCharsets.UTF_8).trim() }.toList()
    }
    assertThat(actual).containsExactlyInAnyOrder(*expected)
  }
  //</editor-fold>
}
